昨天讓 AI agent 寫出了一章正文,也把「好的正文」長什麼樣子寫進了 agent/STYLE.md。
但寫進文件,不代表 agent 一定會照做。Day 18 請 agent 寫完跑 npm run validate,但這個指令其實只檢查 manifest,正文就算引用了不存在的標號、漏放一張截圖,也照樣通過。另外還有一類內容,像法規聲明、安全警語,根本就不該讓 AI 改。
今天要處理的就是這件事:圈出 AI 不能碰的內容,再讓 validate 把正文也檢查一遍。
業務規則、法規聲明、安全警語、售後條款有一個共同特徵:它們的正確性,不是「讀起來合不合理」就能判斷的。AI 可以寫出一段非常像樣的隱私告示,但它不知道公司的法務立場。這些內容錯了,不是文章寫得不夠漂亮的問題,而是法律、安全上的實質風險。
所以這類內容要明確圈起來,AI 可以讀,但不能改。
範例專案 auto-manual-gen 的「新增攝影機」這一章剛好很適合。DemoStreamApp 是 AI 影像監控台,攝影機一啟用推論就會開始分析人員影像,真實產品裡一定會有一段法務審過的告示。所以在昨天那份正文的用途簡介後面,加上一段用 HTML 註解圈起來的內容:
# 新增攝影機
這一章說明怎麼在 DemoStreamApp 建立一台攝影機,並填入它的 RTSP 位址。
<!-- protected:start -->
> 重要:攝影機建立並啟用推論後,會持續拍攝並分析畫面中的人員影像。新增前,請確認安裝位置已依當地法規與場域規定設置監視告示,並取得場域管理者同意。未經同意的影像蒐集,由設置者自負法律責任。
<!-- protected:end -->
## 操作步驟
...
HTML 註解不會出現在最後的文件裡,但機器可以靠它辨認邊界。
(這段告示是我為了範例寫的,真實產品請找法務,千萬不要找 AI XD)
STYLE.md跟昨天一樣,規則不寫在 prompt 裡,而是寫進 agent/STYLE.md:
## 人工保護區
- 原封不動地保留,包含標記本身、換行與標點。不改寫、不搬位置、不刪除、不合併進其他段落。
- 保護區不算骨架裡的小節,也不佔「注意」的三點額度;位置由人決定,重寫時照原位置放回去。
- 覺得保護區的內容或位置有問題,回報給人,不要自己改。
- 不要自己新增保護區。需要警語卻沒有資料時,回報給人。
第二條是為了跟昨天的固定骨架對齊。骨架說「不要自己增加小節」,如果沒講清楚保護區算不算小節,agent 很可能為了遵守骨架,把保護區搬進「注意」或乾脆刪掉。
保護區常見的做法是寫一個「合併器」:先把保護區抽出來、讓 AI 改寫剩下的部分、再塞回去。
但 Day 18 的 agent 是直接寫整份 docs/50-camera-add.md。要加合併器,就得改變 agent 的工作方式,產線也會變得更複雜。(要加其實可以加,只是我不想而已XD)
所以範例專案選擇了另一條路:不限制 agent 怎麼寫,而是在它寫完之後驗證結果。validate 會拿目前的保護區,跟 HEAD 的同一份檔案逐字比對,只要有一個字不同就失敗。這也呼應了範例專案 README 裡的判準:AI 的輸出要嘛凍結成可審查的產物,要嘛被決定性的機制驗證。
validate 也檢查正文保護區只是其中一項。範例專案新增了 runner/docs.ts,npm run validate 在 manifest 通過之後,接著驗證對應的 docs/{order}-{id}.md:
| 檢查 | 等級 | 判定依據 |
|---|---|---|
docs/ 檔名對得回 manifest 的 {order}-{id} |
錯誤 | 命名約定 |
{{legend.<key>}} 是本章 annotate 定義過的 key |
錯誤 | manifest |
| 每張截圖剛好出現一次,沒有引用不存在的截圖 | 錯誤 | manifest |
| 保護區標記成對、沒有巢狀 | 錯誤 | 標記語法 |
| 保護區內容跟上一版完全相同 | 錯誤 | git show HEAD:<file> |
| 「」裡的名稱找得到出處 | 提醒 | i18n、manifest、章節標題、示範資料 |
前五項都有明確的對錯,完全不需要 AI 判斷。Day 18 定下的 {{legend.*}}、{{screenshot:*}} 引用,在這裡就派上用場了:正因為正文不直接寫按鈕文字、不寫圖片路徑,而是引用 manifest 裡的 key,機器才有辦法一一對照。
正文寫了一個 App 根本沒有的按鈕,是最危險的錯誤。手冊可以順利產出、排版完全正常,讀者要到實際操作、找不到按鈕時,才發現手冊是錯的。
這種錯誤沒辦法百分之百靠機器判斷,但可以做一個預警。STYLE.md 規定畫面上的名稱要加「」,而且要照 zh-Hant.json 寫。反過來說,正文裡「」包起來的名稱,應該都找得到出處。
一開始我只拿 i18n 文案、manifest 和示範資料當出處。第一次對現有正文跑 validate,它就把 docs/20-live-monitor.md 裡的「儲存版面設定」標了出來。人看了一下,發現那句是「詳見『儲存版面設定』一章」,指的是另一章的標題,不是畫面上的按鈕。於是把章節標題也加進出處清單,這則提醒就消失了。
如果當初把它設計成錯誤,這次就會卡住整條產線,而且錯的是規則,不是正文;這種常誤判的規則,最後通常會被加進白名單或乾脆關掉。規則能不能明確判定,決定了它應該是錯誤還是提醒。 不確定的新規則,寧可先當提醒,跑過幾輪沒有誤判再升級成錯誤。
為了看看這些檢查實際擋得住什麼,我準備了一份故意改壞的正文 (tools/samples/day19-camera-add-broken.md),放了四個問題:
{{legend.save}},但這一章的 key 是 confirm。camera-add-03。跑出來是這樣:
$ npm run validate -- --chapter camera-add
manifest 驗證通過(1 章)。
需要人工確認(1 則):
- docs/50-camera-add.md: 「快速匯出」在 App 文案、manifest、章節標題與示範資料裡都找不到,請人工確認畫面上真的有這個名稱
正文驗證失敗(3 個問題):
- docs/50-camera-add.md: 引用了不存在的 {{legend.save}},本章可用的有:name / zone / source / enabled / confirm
- docs/50-camera-add.md: 漏放截圖 {{screenshot:camera-add-03}},manifest 裡的每一張都要出現一次
- docs/50-camera-add.md: 第 1 個保護區跟 HEAD 不一致(開頭:「> 重要:攝影機建立並啟用推論後,會持續拍攝並分析畫面中的人…」)。保護區只能由人修改,請用 git diff HEAD -- docs/50-camera-add.md 確認差異,把原文還原回去
最值得一提的是保護區那一條。被改過的版本讀起來完全通順,甚至比原文更簡潔,人工 review 時很可能直接滑過去,但少掉的那一句正好是責任聲明。這種「改得很合理」的錯誤,最適合交給機器逐字比對,而不是靠人的眼睛。
錯誤訊息的寫法則沿用 Day 15 的原則:不只說哪裡錯,也說可用的有哪些、下一步該跑什麼。這些訊息主要是寫給 agent 看的,讀到之後它可以自己修正。
範例專案 clone 下來、切到 chore/day19 分支,就能重現上面那次驗證:
npm run validate -- --chapter camera-add # 審過的版本:通過
# 換成故意改壞的版本
cp tools/samples/day19-camera-add-broken.md docs/50-camera-add.md
npm run validate -- --chapter camera-add # 三個錯誤 + 一則需要人工確認
git checkout -- docs/50-camera-add.md # 復原
如果是在 Windows PowerShell 執行,
--要加上引號,寫成npm run validate '--' --chapter camera-add。沒加引號的--會被 PowerShell 吃掉,--chapter就變成傳給 npm 的參數,結果會驗證全部章節,跟上面的輸出對不起來。
也可以自己動手改改看,例如把保護區的 protected:end 刪掉,validate 會直接告訴你第幾行的 protected:start 沒有對應的結尾。
保護區是跟 HEAD 比的,所以人審過並 commit 之後,那一版就成為下一次驗證的基準。反過來說,如果 agent 改了保護區、人沒注意到就 commit 了,錯誤的版本也會跟著變成基準。
所以在 CI 裡驗證整個 PR 時,建議用 --base main 跟主分支比,而不是跟 PR 裡的上一個 commit 比。這樣就算 PR 中途有 commit 動過保護區,最後還是會被擋下來。
今天用 HTML 註解圈出人工保護區,並讓 validate 在 manifest 之後接著檢查正文:有明確對錯的擋下來,判斷不了的只提醒。
讓 AI Agent 自動組裝產線的部分差不多到這裡結束。前面幾天建立了從畫面探勘、宣告式 manifest、agent 產出到驗收的流程。明天開始進入下一階段,把這些 Markdown 內容真正交付成客戶收得下的 Word 與 PDF 文件。